package log

import (
	"context"
	"github.com/gin-gonic/gin"
	"github.com/natefinch/lumberjack"
	config2 "go-gin-api/core/config"
	"go-gin-api/exception"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"io"
	"net"
	"net/http"
	"net/http/httputil"
	"os"
	"runtime/debug"
	"strings"
	"time"
)

const (
	DOMAIN        = "Domain"        // 域 api/crontab/queue
	KINGDOM       = "Kingdom"       // 界 dev/prod
	PHYLUM        = "Phylum"        // 门 logic/mysql/redis/request
	CLASS         = "Class"         // 纲 队列名称,模块名称
	ORDER         = "Order"         // 目 corpId CID
	FAMILY        = "Family"        // 科 operate 实际操作人
	GENUS         = "Genus"         // 属 userId
	SPECIES       = "Species"       // 种 requestId
	EXECUTIONTIME = "ExecutionTime" // 执行时长
	RESPCODE      = "RespCode"      // 返回码
)

//只能输出结构化日志，但是性能要高于 SugaredLogger
var logger *zap.Logger

//可以输出 结构化日志、非结构化日志。性能茶语 zap.Logger，具体可见上面的的单元测试
var sugarLogger *zap.SugaredLogger

// InitLog 初始化日志 logger
func InitLog(logLevel zapcore.Level) {
	config := zapcore.EncoderConfig{
		MessageKey:   "msg",                       //结构化（json）输出：msg的key
		LevelKey:     "level",                     //结构化（json）输出：日志级别的key（INFO，WARN，ERROR等）
		TimeKey:      "ts",                        //结构化（json）输出：时间的key（INFO，WARN，ERROR等）
		CallerKey:    "file",                      //结构化（json）输出：打印日志的文件对应的Key
		EncodeLevel:  zapcore.CapitalLevelEncoder, //将日志级别转换成大写（INFO，WARN，ERROR等）
		EncodeCaller: zapcore.ShortCallerEncoder,  //采用短文件路径编码输出（test/main.go:14	）
		EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
			enc.AppendString(t.Format("2006-01-02 15:04:05"))
		}, //输出的时间格式
		EncodeDuration: func(d time.Duration, enc zapcore.PrimitiveArrayEncoder) {
			enc.AppendInt64(int64(d) / 1000000)
		}, //
	}
	//自定义日志级别：自定义Info级别
	infoLevel := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
		return lvl < zapcore.WarnLevel && lvl >= logLevel
	})

	//自定义日志级别：自定义Warn级别
	warnLevel := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
		return lvl >= zapcore.WarnLevel && lvl >= logLevel
	})

	// 获取io.Writer的实现
	infoWriter := getWriter(config2.GetString("log.path.err"))
	warnWriter := getWriter(config2.GetString("log.path.info"))

	// 实现多个输出
	core := zapcore.NewTee(
		zapcore.NewCore(zapcore.NewConsoleEncoder(config), zapcore.AddSync(infoWriter), infoLevel),                         //将info及以下写入logPath，NewConsoleEncoder 是非结构化输出
		zapcore.NewCore(zapcore.NewConsoleEncoder(config), zapcore.AddSync(warnWriter), warnLevel),                         //warn及以上写入errPath
		zapcore.NewCore(zapcore.NewJSONEncoder(config), zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout)), logLevel), //同时将日志输出到控制台，NewJSONEncoder 是结构化输出
	)
	logger = zap.New(core, zap.AddCaller(), zap.AddStacktrace(zap.WarnLevel))
	sugarLogger = logger.Sugar()
}

func getWriter(filename string) io.Writer {
	return &lumberjack.Logger{
		Filename:   filename,
		MaxSize:    10,    //最大M数，超过则切割
		MaxBackups: 5,     //最大文件保留数，超过就删除最老的日志文件
		MaxAge:     30,    //保存30天
		Compress:   false, //是否压缩
	}
}

// GinLogger 接收gin框架默认的日志
func GinLogger() gin.HandlerFunc {
	return func(c *gin.Context) {
		start := time.Now()
		path := c.Request.URL.Path
		query := c.Request.URL.RawQuery
		c.Next()

		cost := time.Since(start)
		logger.Info(path,
			zap.Int("status", c.Writer.Status()),
			zap.String("method", c.Request.Method),
			zap.String("path", path),
			zap.String("query", query),
			zap.String("ip", c.ClientIP()),
			zap.String("user-agent", c.Request.UserAgent()),
			zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),
			zap.Duration("cost", cost),
		)
	}
}

func Ctx(ctx context.Context) *zap.SugaredLogger {
	if ctx == nil {
		ctx = CacheCtx()
	}
	l := sugarLogger
	return parse(ctx, l)
}

func Error() *zap.SugaredLogger {
	ctx := CacheCtx()
	l := sugarLogger
	return parse(ctx, l)
}

func parse(ctx context.Context, l *zap.SugaredLogger) *zap.SugaredLogger {
	if v, ok := ctx.Value(DOMAIN).(string); ok {
		l = l.With(DOMAIN, v)
	} else {
		ctx = CacheCtx()
		l = l.With(DOMAIN, exception.Domain)
	}
	if v, ok := ctx.Value(KINGDOM).(string); ok {
		l = l.With(KINGDOM, v)
	}
	if v, ok := ctx.Value(PHYLUM).(string); ok {
		l = l.With(PHYLUM, v)
	}
	if v, ok := ctx.Value(CLASS).(string); ok {
		l = l.With(CLASS, v)
	}
	if v, ok := ctx.Value(ORDER).(string); ok {
		l = l.With(ORDER, v)
	}
	if v, ok := ctx.Value(FAMILY).(string); ok {
		l = l.With(FAMILY, v)
	}
	if v, ok := ctx.Value(GENUS).(string); ok {
		l = l.With(GENUS, v)
	}
	if v, ok := ctx.Value(SPECIES).(string); ok {
		l = l.With(SPECIES, v)
	}
	return l
}

func CacheCtx() context.Context {
	return context.WithValue(context.WithValue(context.Background(), DOMAIN, exception.Domain), KINGDOM, exception.RunMode)
}

// GinRecovery recover掉项目可能出现的panic，并使用zap记录相关日志
func GinRecovery(stack bool) gin.HandlerFunc {
	return func(c *gin.Context) {
		defer func() {
			if err := recover(); err != nil {
				// Check for a broken connection, as it is not really a
				// condition that warrants a panic stack trace.
				var brokenPipe bool
				if ne, ok := err.(*net.OpError); ok {
					if se, ok := ne.Err.(*os.SyscallError); ok {
						if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
							brokenPipe = true
						}
					}
				}

				httpRequest, _ := httputil.DumpRequest(c.Request, false)
				if brokenPipe {
					logger.Error(c.Request.URL.Path,
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
					)
					// If the connection is dead, we can't write a status to it.
					c.Error(err.(error)) // nolint: errcheck
					c.Abort()
					return
				}

				if stack {
					logger.Error("logger", zap.String("name", "修华师7"))
					logger.Error("[Recovery from panic]",
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
						zap.String("stack", string(debug.Stack())),
					)
				} else {
					logger.Error("[Recovery from panic]",
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
					)
				}
				c.AbortWithStatus(http.StatusInternalServerError)
			}
		}()
		c.Next()
	}
}
